En omfattende guide til Reacts createPortal API, som dekker teknikker for å lage portaler, strategier for hendelseshåndtering og avanserte bruksområder.
React createPortal: Mestring av portal-opprettelse og hendelseshåndtering
I moderne webutvikling med React er det avgjørende å skape brukergrensesnitt som sømløst integreres med den underliggende dokumentstrukturen. Selv om Reacts komponentmodell er utmerket for å håndtere det virtuelle DOM-et, trenger vi noen ganger å gjengi elementer utenfor det normale komponenthierarkiet. Det er her createPortal kommer inn. Denne guiden utforsker createPortal i dybden, og dekker formålet, bruken og avanserte teknikker for å håndtere hendelser og bygge komplekse UI-elementer. Vi vil dekke hensyn til internasjonalisering, beste praksis for tilgjengelighet og vanlige fallgruver man bør unngå.
Hva er React createPortal?
createPortal er et React API som lar deg gjengi et React-komponents barn i en annen del av DOM-treet, utenfor overkomponentens hierarki. Dette er spesielt nyttig for å lage elementer som modaler, verktøytips, nedtrekksmenyer og overlegg som må plasseres på øverste nivå i dokumentet eller i en bestemt beholder, uavhengig av hvor komponenten som utløser dem er plassert i Reacts komponenttre.
Uten createPortal innebærer dette ofte komplekse løsninger som å manipulere DOM-et direkte eller bruke CSS absolutt posisjonering, noe som kan føre til problemer med stacking contexts, z-index-konflikter og tilgjengelighet.
Hvorfor bruke createPortal?
Her er de viktigste grunnene til at createPortal er et verdifullt verktøy i ditt React-arsenal:
- Forbedret DOM-struktur: Unngår å nøste komponenter dypt i DOM-et, noe som fører til en renere og mer håndterbar struktur. Dette er spesielt viktig for komplekse applikasjoner med mange interaktive elementer.
- Forenklet styling: Posisjoner elementer enkelt i forhold til viewporten eller spesifikke beholdere uten å måtte ty til komplekse CSS-triks. Dette forenkler styling og layout, spesielt når man håndterer elementer som må ligge over annet innhold.
- Forbedret tilgjengelighet: Gjør det enklere å skape tilgjengelige brukergrensesnitt ved å la deg håndtere fokus og tastaturnavigasjon uavhengig av komponenthierarkiet. For eksempel å sikre at fokus forblir innenfor et modalvindu.
- Bedre hendelseshåndtering: Lar hendelser propagere korrekt fra portalens innhold til React-treet, og sikrer at hendelseslyttere festet til overkomponenter fortsatt fungerer som forventet.
Grunnleggende bruk av createPortal
createPortal API-et aksepterer to argumenter:
- React-noden (JSX) du vil gjengi.
- DOM-elementet der du vil gjengi noden. Dette DOM-elementet bør ideelt sett eksistere før komponenten som bruker
createPortalmonteres.
Her er et enkelt eksempel:
Eksempel: Gengi en modal
La oss si du har en modal-komponent som du vil gjengi på slutten av body-elementet.
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
const modalRoot = document.getElementById('modal-root'); // Antar at du har en <div id="modal-root"></div> i HTML-en din
if (!modalRoot) {
console.error('Modal root element not found!');
return null;
}
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
modalRoot
);
}
export default Modal;
Forklaring:
- Vi importerer
ReactDOMfordicreatePortaler en metode påReactDOM-objektet. - Vi antar at det er et DOM-element med ID-en
modal-rooti HTML-en din. Det er her modalen vil bli gjengitt. Sørg for at dette elementet eksisterer. En vanlig praksis er å legge til en<div id="modal-root"></div>rett før den avsluttende</body>-taggen iindex.html-filen din. - Vi bruker
ReactDOM.createPortalfor å gjengi modalens JSX imodalRoot-elementet. - Vi bruker
e.stopPropagation()for å forhindre atonClick-hendelsen på modalens innhold utløseronClose-handleren på overlegget. Dette sikrer at klikk inne i modalen ikke lukker den.
Bruk:
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Modal Content</h2>
<p>This is the content of the modal.</p>
<button onClick={() => setIsModalOpen(false)}>Close</button>
</Modal>
</div>
);
}
export default App;
Dette eksempelet demonstrerer hvordan man gjengir en modal utenfor det normale komponenthierarkiet, noe som lar deg posisjonere den absolutt på siden. Å bruke createPortal på denne måten løser vanlige problemer med stacking contexts og lar deg enkelt lage konsistent modal-styling i hele applikasjonen.
Hendelseshåndtering med createPortal
En av de viktigste fordelene med createPortal er at den bevarer den normale hendelsesboblings-atferden i React. Dette betyr at hendelser som oppstår i portalens innhold, fortsatt vil propagere oppover Reacts komponenttre, slik at overkomponenter kan håndtere dem.
Det er imidlertid viktig å forstå hvordan hendelser håndteres når de krysser portal-grensen.
Eksempel: Håndtere hendelser utenfor portalen
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function OutsideClickExample() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const portalRoot = document.getElementById('portal-root');
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [dropdownRef]);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle Dropdown</button>
{isOpen && portalRoot && ReactDOM.createPortal(
<div ref={dropdownRef} style={{ position: 'absolute', top: '50px', left: '0', border: '1px solid black', padding: '10px', backgroundColor: 'white' }}>
Dropdown Content
</div>,
portalRoot
)}
</div>
);
}
export default OutsideClickExample;
Forklaring:
- Vi bruker en
reffor å få tilgang til nedtrekkselementet som gjengis inne i portalen. - Vi fester en
mousedownhendelseslytter tildocumentfor å oppdage klikk utenfor nedtrekksmenyen. - Inne i hendelseslytteren sjekker vi om klikket skjedde utenfor nedtrekksmenyen ved hjelp av
dropdownRef.current.contains(event.target). - Hvis klikket skjedde utenfor nedtrekksmenyen, lukker vi den ved å sette
isOpentilfalse.
Dette eksempelet demonstrerer hvordan man håndterer hendelser som skjer utenfor portalens innhold, slik at du kan lage interaktive elementer som responderer på brukerhandlinger i det omkringliggende dokumentet.
Avanserte bruksområder
createPortal er ikke begrenset til enkle modaler og verktøytips. Det kan brukes i ulike avanserte scenarier, inkludert:
- Kontekstmenyer: Gjengi kontekstmenyer dynamisk nær musepekeren ved høyreklikk.
- Varsler: Vis varsler øverst på skjermen, uavhengig av komponenthierarkiet.
- Egendefinerte popovers: Lag egendefinerte popover-komponenter med avansert posisjonering og styling.
- Integrasjon med tredjepartsbiblioteker: Bruk
createPortaltil å integrere React-komponenter med tredjepartsbiblioteker som krever spesifikke DOM-strukturer.
Eksempel: Lage en kontekstmeny
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function ContextMenuExample() {
const [contextMenu, setContextMenu] = useState(null);
const menuRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setContextMenu(null);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [menuRef]);
const handleContextMenu = (event) => {
event.preventDefault();
setContextMenu({
x: event.clientX,
y: event.clientY,
});
};
const portalRoot = document.getElementById('portal-root');
return (
<div onContextMenu={handleContextMenu} style={{ border: '1px solid black', padding: '20px' }}>
Right-click here to open context menu
{contextMenu && portalRoot && ReactDOM.createPortal(
<div
ref={menuRef}
style={{
position: 'absolute',
top: contextMenu.y,
left: contextMenu.x,
border: '1px solid black',
padding: '10px',
backgroundColor: 'white',
}}
>
<ul>
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
</div>,
portalRoot
)}
</div>
);
}
export default ContextMenuExample;
Forklaring:
- Vi bruker
onContextMenu-hendelsen for å oppdage høyreklikk på målelementet. - Vi forhindrer at standardkontekstmenyen vises ved hjelp av
event.preventDefault(). - Vi lagrer musekoordinatene i
contextMenu-tilstandsvariabelen. - Vi gjengir kontekstmenyen inne i en portal, posisjonert ved musekoordinatene.
- Vi inkluderer den samme logikken for å oppdage klikk utenfor som i forrige eksempel for å lukke kontekstmenyen når brukeren klikker utenfor den.
Hensyn til tilgjengelighet
Når du bruker createPortal, er det avgjørende å ta hensyn til tilgjengelighet for å sikre at applikasjonen din kan brukes av alle.
Fokushåndtering
Når en portal åpnes (f.eks. en modal), bør du sikre at fokus automatisk flyttes til det første interaktive elementet i portalen. Dette hjelper brukere som navigerer med tastatur eller skjermleser med å enkelt få tilgang til portalens innhold.
Når portalen lukkes, bør du returnere fokus til elementet som utløste åpningen av portalen. Dette opprettholder en konsistent navigeringsflyt.
ARIA-attributter
Bruk ARIA-attributter for å gi semantisk informasjon om portalens innhold. For eksempel, bruk aria-modal="true" på modalelementet for å indikere at det er en modal dialogboks. Bruk aria-labelledby for å knytte modalen til tittelen, og aria-describedby for å knytte den til beskrivelsen.
Tastaturnavigasjon
Sørg for at brukere kan navigere i portalens innhold ved hjelp av tastaturet. Bruk tabindex-attributtet for å kontrollere fokusrekkefølgen, og sørg for at alle interaktive elementer er nåbare med tastaturet.
Vurder å fange fokus inne i portalen slik at brukere ikke ved et uhell kan navigere utenfor den. Dette kan oppnås ved å lytte etter Tab-tasten og programmatisk flytte fokus til det første eller siste interaktive elementet i portalen.
Eksempel: Tilgjengelig modal
import React, { useState, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
function AccessibleModal({ children, isOpen, onClose, labelledBy, describedBy }) {
const modalRef = useRef(null);
const firstFocusableElementRef = useRef(null);
const [previouslyFocusedElement, setPreviouslyFocusedElement] = useState(null);
const modalRoot = document.getElementById('modal-root');
useEffect(() => {
if (isOpen) {
// Lagre elementet som har fokus før modalen åpnes.
setPreviouslyFocusedElement(document.activeElement);
// Fokuser på det første fokuserbare elementet i modalen.
if (firstFocusableElementRef.current) {
firstFocusableElementRef.current.focus();
}
// Fang fokus innenfor modalen.
function handleKeyDown(event) {
if (event.key === 'Tab') {
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusableElement = focusableElements[0];
const lastFocusableElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey) {
// Shift + Tab
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
event.preventDefault();
}
} else {
// Tab
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
event.preventDefault();
}
}
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Gjenopprett fokus til elementet som hadde fokus før modalen ble åpnet.
if(previouslyFocusedElement && previouslyFocusedElement.focus) {
previouslyFocusedElement.focus();
}
};
}
}, [isOpen, previouslyFocusedElement]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<div
className="modal-overlay"
onClick={onClose}
aria-modal="true"
aria-labelledby={labelledBy}
aria-describedby={describedBy}
ref={modalRef}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2 id={labelledBy}>Modal Title</h2>
<p id={describedBy}>This is the modal content.</p>
<button ref={firstFocusableElementRef} onClick={onClose}>
Close
</button>
{children}
</div>
</div>,
modalRoot
);
}
export default AccessibleModal;
Forklaring:
- Vi bruker ARIA-attributter som
aria-modal,aria-labelledbyogaria-describedbyfor å gi semantisk informasjon om modalen. - Vi bruker
useEffect-hooken for å håndtere fokus når modalen åpnes og lukkes. - Vi lagrer det elementet som har fokus før modalen åpnes og gjenoppretter fokus til det når modalen lukkes.
- Vi fanger fokus inne i modalen ved hjelp av en
keydownhendelseslytter.
Hensyn til internasjonalisering (i18n)
Når man utvikler applikasjoner for et globalt publikum, er internasjonalisering (i18n) en kritisk faktor. Når du bruker createPortal, er det noen punkter å huske på:
- Tekstretning (RTL/LTR): Sørg for at stylingen din tar hensyn til både venstre-til-høyre (LTR) og høyre-til-venstre (RTL) språk. Dette kan innebære bruk av logiske egenskaper i CSS (f.eks.
margin-inline-starti stedet formargin-left) og å settedir-attributtet riktig på HTML-elementet. - Lokalisering av innhold: All tekst i portalen bør lokaliseres til brukerens foretrukne språk. Bruk et i18n-bibliotek (f.eks.
react-intl,i18next) for å håndtere oversettelser. - Formatering av tall og datoer: Formater tall og datoer i henhold til brukerens lokalitet.
IntlAPI-et gir funksjonalitet for dette. - Kulturelle konvensjoner: Vær oppmerksom på kulturelle konvensjoner knyttet til UI-elementer. For eksempel kan plassering av knapper variere mellom kulturer.
Eksempel: i18n med react-intl
import React from 'react';
import { FormattedMessage } from 'react-intl';
function MyComponent() {
return (
<div>
<FormattedMessage id="myComponent.greeting" defaultMessage="Hello, world!" />
</div>
);
}
export default MyComponent;
FormattedMessage-komponenten fra react-intl henter den oversatte meldingen basert på brukerens lokalitet. Konfigurer react-intl med dine oversettelser for forskjellige språk.
Vanlige fallgruver og løsninger
Selv om createPortal er et kraftig verktøy, er det viktig å være klar over noen vanlige fallgruver og hvordan man kan unngå dem:
- Mangler portal-rotelement: Sørg for at DOM-elementet du bruker som portal-rot eksisterer før komponenten som bruker
createPortalmonteres. En god praksis er å plassere det direkte iindex.html. - Z-index-konflikter: Vær oppmerksom på z-index-verdier når du posisjonerer elementer med
createPortal. Bruk CSS for å håndtere stacking contexts og sikre at portalens innhold vises korrekt. - Problemer med hendelseshåndtering: Forstå hvordan hendelser propagerer gjennom portalen og håndter dem på riktig måte. Bruk
e.stopPropagation()for å forhindre at hendelser utløser utilsiktede handlinger. - Minnelekkasjer: Rydd opp i hendelseslyttere og referanser på riktig måte når komponenten som bruker
createPortalavmonteres for å unngå minnelekkasjer. BrukuseEffect-hooken med en oppryddingsfunksjon for å oppnå dette. - Uventede rulleproblemer: Portaler kan noen ganger forstyrre den forventede rulleatferden på siden. Sørg for at stilene dine ikke forhindrer rulling og at modale elementer ikke forårsaker sidehopp eller uventet rulleatferd når de åpnes og lukkes.
Konklusjon
React.createPortal er et verdifullt verktøy for å skape fleksible, tilgjengelige og vedlikeholdbare brukergrensesnitt i React. Ved å forstå formålet, bruken og avanserte teknikker for å håndtere hendelser og tilgjengelighet, kan du utnytte kraften til å bygge komplekse og engasjerende webapplikasjoner som gir en overlegen brukeropplevelse for et globalt publikum. Husk å ta hensyn til beste praksis for internasjonalisering og tilgjengelighet for å sikre at applikasjonene dine er inkluderende og kan brukes av alle.
Ved å følge retningslinjene og eksemplene i denne guiden, kan du trygt bruke createPortal til å løse vanlige UI-utfordringer og skape fantastiske webopplevelser.